/*
 * Created on 10 Jul 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.peermanager.nat;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.Average;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.nat.NATTraversal;
import com.aelitis.azureus.core.nat.NATTraversalHandler;
import com.aelitis.azureus.core.nat.NATTraversalObserver;
import com.aelitis.azureus.core.nat.NATTraverser;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

public class PeerNATTraverser implements NATTraversalHandler {
    private static final LogIDs LOGID = LogIDs.PEER;

    private static final int OUTCOME_SUCCESS = 0;
    private static final int OUTCOME_FAILED_NO_REND = 1;
    private static final int OUTCOME_FAILED_OTHER = 2;

    private static PeerNATTraverser singleton;

    public static void initialise(AzureusCore core) {
        singleton = new PeerNATTraverser(core);
    }

    public static PeerNATTraverser getSingleton() {
        return (singleton);
    }

    private static int MAX_ACTIVE_REQUESTS;

    static {
        COConfigurationManager.addAndFireParameterListener("peer.nat.traversal.request.conc.max", new ParameterListener() {
            public void parameterChanged(String name) {
                MAX_ACTIVE_REQUESTS = COConfigurationManager.getIntParameter(name);
            }
        });
    }

    private static final int TIMER_PERIOD = 10 * 1000;
    private static final int USAGE_PERIOD = TIMER_PERIOD;
    private static final int USAGE_DURATION_SECS = 60;
    private static final int MAX_USAGE_PER_MIN = MAX_ACTIVE_REQUESTS * 5 * 1000;

    private static final int STATS_TICK_COUNT = 120 * 1000 / TIMER_PERIOD;

    private NATTraverser nat_traverser;

    private Map initiators = new HashMap();
    private LinkedList pending_requests = new LinkedList();
    private List active_requests = new ArrayList();

    private Average usage_average = Average.getInstance(USAGE_PERIOD, USAGE_DURATION_SECS);

    private int attempted_count = 0;
    private int success_count = 0;
    private int failed_no_rendezvous = 0;
    private int failed_negative_bloom = 0;

    private BloomFilter negative_result_bloom = BloomFilterFactory.createAddOnly(BLOOM_SIZE);

    private static final int BLOOM_SIZE = MAX_ACTIVE_REQUESTS * 1024;
    private static final int BLOOM_REBUILD_PERIOD = 5 * 60 * 1000;
    private static final int BLOOM_REBUILD_TICKS = BLOOM_REBUILD_PERIOD / TIMER_PERIOD;

    private PeerNATTraverser(AzureusCore core) {
        nat_traverser = core.getNATTraverser();

        nat_traverser.registerHandler(this);

        SimpleTimer.addPeriodicEvent("PeerNAT:stats", TIMER_PERIOD, new TimerEventPerformer() {
            private int ticks;

            public void perform(TimerEvent event) {
                ticks++;

                List to_run = null;

                synchronized (initiators) {

                    if (ticks % BLOOM_REBUILD_TICKS == 0) {

                        int size = negative_result_bloom.getEntryCount();

                        if (Logger.isEnabled()) {

                            if (size > 0) {

                                Logger.log(new LogEvent(LOGID, "PeerNATTraverser: negative bloom size = " + size));
                            }
                        }

                        negative_result_bloom = BloomFilterFactory.createAddOnly(BLOOM_SIZE);
                    }

                    if (ticks % STATS_TICK_COUNT == 0) {

                        String msg =
                                "NAT traversal stats: active=" + active_requests.size() + ",pending=" + pending_requests.size() + ",attempted="
                                        + attempted_count + ",no rendezvous=" + failed_no_rendezvous + ",negative bloom=" + failed_negative_bloom
                                        + ",successful=" + success_count;

                        // System.out.println( msg );

                        if (Logger.isEnabled()) {
                            Logger.log(new LogEvent(LOGID, msg));
                        }
                    }

                    int used = 0;

                    for (int i = 0; i < active_requests.size(); i++) {

                        used += ((PeerNATTraversal) active_requests.get(i)).getTimeUsed();
                    }

                    usage_average.addValue(used);

                    int usage = (int) usage_average.getAverage();

                    if (usage > MAX_USAGE_PER_MIN) {

                        return;
                    }

                    // System.out.println( "usage = " + usage );

                    while (true) {

                        if (pending_requests.size() == 0 || active_requests.size() >= MAX_ACTIVE_REQUESTS) {

                            break;
                        }

                        // TODO: prioritisation based on initiator connections etc?

                        PeerNATTraversal traversal = (PeerNATTraversal) pending_requests.removeFirst();

                        active_requests.add(traversal);

                        if (to_run == null) {

                            to_run = new ArrayList();
                        }

                        to_run.add(traversal);

                        attempted_count++;
                    }
                }

                if (to_run != null) {

                    for (int i = 0; i < to_run.size(); i++) {

                        PeerNATTraversal traversal = (PeerNATTraversal) to_run.get(i);

                        boolean bad = false;

                        synchronized (initiators) {

                            if (negative_result_bloom.contains(traversal.getTarget().toString().getBytes())) {

                                bad = true;

                                failed_negative_bloom++;
                            }
                        }

                        if (bad) {

                            removeRequest(traversal, OUTCOME_FAILED_OTHER);

                            traversal.getAdapter().failed();
                        } else {

                            traversal.run();
                        }
                    }
                }
            }
        });
    }

    public int getType() {
        return (NATTraverser.TRAVERSE_REASON_PEER_DATA);
    }

    public String getName() {
        return ("Peer Traversal");
    }

    public void register(PeerNATInitiator initiator) {
        synchronized (initiators) {

            if (initiators.put(initiator, new LinkedList()) != null) {

                Debug.out("initiator already present");
            }
        }
    }

    public void unregister(PeerNATInitiator initiator) {
        List to_cancel;

        synchronized (initiators) {

            LinkedList requests = (LinkedList) initiators.remove(initiator);

            if (requests == null) {

                Debug.out("initiator not present");

                return;

            } else {

                to_cancel = requests;
            }
        }

        Iterator it = to_cancel.iterator();

        while (it.hasNext()) {

            PeerNATTraversal traversal = (PeerNATTraversal) it.next();

            traversal.cancel();
        }
    }

    public void create(PeerNATInitiator initiator, InetSocketAddress target, PeerNATTraversalAdapter adapter) {
        boolean bad = false;

        synchronized (initiators) {

            if (negative_result_bloom.contains(target.toString().getBytes())) {

                bad = true;

                failed_negative_bloom++;

            } else {

                LinkedList requests = (LinkedList) initiators.get(initiator);

                if (requests == null) {

                    // we get here when download stopped at same time
                    // Debug.out( "initiator not found" );

                    bad = true;

                } else {

                    PeerNATTraversal traversal = new PeerNATTraversal(initiator, target, adapter);

                    requests.addLast(traversal);

                    pending_requests.addLast(traversal);

                    if (Logger.isEnabled()) {
                        Logger.log(new LogEvent(LOGID, "created NAT traversal for " + initiator.getDisplayName() + "/" + target));
                    }
                }
            }
        }

        if (bad) {

            adapter.failed();
        }
    }

    public List getTraversals(PeerNATInitiator initiator) {
        List result = new ArrayList();

        synchronized (initiators) {

            LinkedList requests = (LinkedList) initiators.get(initiator);

            if (requests != null) {

                Iterator it = requests.iterator();

                while (it.hasNext()) {

                    PeerNATTraversal x = (PeerNATTraversal) it.next();

                    result.add(x.getTarget());
                }
            }
        }

        return (result);
    }

    protected void removeRequest(PeerNATTraversal request, int outcome) {
        synchronized (initiators) {

            LinkedList requests = (LinkedList) initiators.get(request.getInitiator());

            if (requests != null) {

                requests.remove(request);
            }

            pending_requests.remove(request);

            if (active_requests.remove(request)) {

                usage_average.addValue(request.getTimeUsed());

                if (outcome == OUTCOME_SUCCESS) {

                    success_count++;

                } else {

                    InetSocketAddress target = request.getTarget();

                    negative_result_bloom.add(target.toString().getBytes());

                    if (outcome == OUTCOME_FAILED_NO_REND) {

                        failed_no_rendezvous++;
                    }
                }
            }
        }
    }

    public Map process(InetSocketAddress originator, Map data) {
        // System.out.println( "PeerNAT: received traversal from " + originator );

        return (null);
    }

    protected class PeerNATTraversal implements NATTraversalObserver {
        private PeerNATInitiator initiator;
        private InetSocketAddress target;
        private PeerNATTraversalAdapter adapter;

        private NATTraversal traversal;
        private boolean cancelled;

        private long time;

        protected PeerNATTraversal(PeerNATInitiator _initiator, InetSocketAddress _target, PeerNATTraversalAdapter _adapter) {
            initiator = _initiator;
            target = _target;
            adapter = _adapter;
        }

        protected PeerNATInitiator getInitiator() {
            return (initiator);
        }

        protected InetSocketAddress getTarget() {
            return (target);
        }

        protected PeerNATTraversalAdapter getAdapter() {
            return (adapter);
        }

        protected long getTimeUsed() {
            long now = SystemTime.getCurrentTime();

            long elapsed = now - time;

            time = now;

            if (elapsed < 0) {

                elapsed = 0;

            } else {
                // sanity check

                elapsed = Math.min(elapsed, TIMER_PERIOD);
            }

            return (elapsed);
        }

        protected void run() {
            synchronized (this) {

                if (!cancelled) {

                    time = SystemTime.getCurrentTime();

                    traversal = nat_traverser.attemptTraversal(PeerNATTraverser.this, target, null, false, this);
                }
            }
        }

        public void succeeded(InetSocketAddress rendezvous, InetSocketAddress target, Map reply) {
            removeRequest(this, OUTCOME_SUCCESS);

            if (Logger.isEnabled()) {
                Logger.log(new LogEvent(LOGID, "NAT traversal for " + initiator.getDisplayName() + "/" + target + " succeeded"));
            }

            adapter.success(target);
        }

        public void failed(int reason) {
            removeRequest(this, reason == NATTraversalObserver.FT_NO_RENDEZVOUS ? OUTCOME_FAILED_NO_REND : OUTCOME_FAILED_OTHER);

            adapter.failed();
        }

        public void failed(Throwable cause) {
            removeRequest(this, OUTCOME_FAILED_OTHER);

            adapter.failed();
        }

        public void disabled() {
            removeRequest(this, OUTCOME_FAILED_OTHER);

            adapter.failed();
        }

        protected void cancel() {
            NATTraversal active_traversal;

            synchronized (this) {

                cancelled = true;

                active_traversal = traversal;
            }

            if (active_traversal == null) {

                removeRequest(this, OUTCOME_FAILED_OTHER);

            } else {

                active_traversal.cancel();
            }

            adapter.failed();
        }
    }
}
